package com.hwd.gmall.seckill.task;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hwd.gmall.common.constant.SeckillConst;
import com.hwd.gmall.common.enums.SeckillGoodsStatusEnum;
import com.hwd.gmall.model.activity.SeckillGoods;
import com.hwd.gmall.seckill.mapper.SeckillGoodsMapper;
import com.hwd.gmall.seckill.util.DateUtil;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
 * 将秒杀商品定时从数据库中同步到redis中的定时任务
 *
 * @author 黄伟东/Victor
 * @date 2022/5/11 15:39
 */
@Component
public class SeckillGoodsFromDbToRedisTask {

    @Resource
    private SeckillGoodsMapper seckillGoodsMapper;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 将秒杀商品定时从数据库中同步到redis中去
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    public void secKillGoodsFromDbToRedisTask() {

        // 获取当前系统所在的时间段和后续 4 个时间段，12个小时，5个时间段
        List<Date> dateMenus = DateUtil.getDateMenus();

        // 遍历5个时间段，获取每个时间段的商品列表数据
        dateMenus.forEach(date -> {

            // 获取开始时间
            String startTime = DateUtil.data2str(date, DateUtil.PATTERN_YYYY_MM_DDHHMM);
            // 获取结束时间日期
            Date endDate = DateUtil.addDateHour(date, 2);
            // 计算当前时间段的商品存活时间
            long timeout = endDate.getTime() - System.currentTimeMillis();
            // 获取结束时间
            String endTime = DateUtil.data2str(endDate, DateUtil.PATTERN_YYYY_MM_DDHHMM);
            // 获取时间段的key = 2022051116
            String time = DateUtil.data2str(date, DateUtil.PATTERN_YYYYMMDDHH);

            // 获取当前时间段的全部hKey
            Set<Object> seckillGoodsIds = redisTemplate.opsForHash().keys(time);

            // 遍历每个时间段符合条件的商品
            LambdaQueryWrapper<SeckillGoods> queryWrapper = new LambdaQueryWrapper<SeckillGoods>()
                    // 1、状态校验，1:审核通过
                    .eq(SeckillGoods::getStatus, SeckillGoodsStatusEnum.ONE.getStatus())
                    // 2、库存校验，库存要大于零
                    .gt(SeckillGoods::getStockCount, 0)
                    // 3、商品的活动时间是否在指定范围内 start_time >= startTime，end_time <= endTime
                    .ge(SeckillGoods::getStartTime, startTime).le(SeckillGoods::getEndTime, endTime)
                    // 4、redis中不存在
                    .notIn(seckillGoodsIds.size() > 0, SeckillGoods::getId, seckillGoodsIds);

            // 查询秒杀商品列表
            List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(queryWrapper);

            // 将商品一个个存储到redis中去，使用hash类型，time:时间段(20220511)，hk:seckillGoodsIds,hv:goods
            seckillGoodsList.forEach(seckillGoods -> {

                // 获取商品id
                Long goodsId = seckillGoods.getId();
                // 获取商品的库存
                Integer stockCount = seckillGoods.getStockCount();
                // 构建一个商品库存长度和元素个数的数组
                String[] goodsIds = getGoodsIds(goodsId + "", stockCount);

                // 1、保存商品数据
                redisTemplate.opsForHash().put(time, goodsId + "", seckillGoods);

                // 2、构建商品库存剩余个数长度的队列，商品库存的队列设置过期时间
                String stockCountKey = SeckillConst.SECKILL_GOODS_STOCK_COUNT_QUEUE + goodsId;
                redisTemplate.opsForList().leftPushAll(stockCountKey, goodsIds);
                redisTemplate.expire(stockCountKey, timeout, TimeUnit.MILLISECONDS);

                // 3、为每个商品准备一个库存的自增值：自增的数量就是商品的库存数
                redisTemplate.opsForHash().increment(SeckillConst.SECKILL_GOODS_STOCK + time, goodsId + "",
                        stockCount);

            });

            // 设置当前时间段的商品过期时间，需要计算存活时间，只要设置一次
            setSeckillGoodsTimeOut(time, timeout);
        });

    }

    /**
     * 给指定的商品创建一个数组，长度为商品库存长度，元素全部是当前商品id
     *
     * @param goodsId    商品id
     * @param stockCount 商品库存
     * @return java.lang.String[]
     */
    private String[] getGoodsIds(String goodsId, Integer stockCount) {
        return IntStream.range(0, stockCount)
                .mapToObj(i -> goodsId)
                .toArray(String[]::new);
    }

    /**
     * 设置当前时间段的商品过期时间，每个时间段只要设置一次
     *
     * @param time    redis中指定时间段商品列表的key
     * @param timeout 指定时间段商品的过期时间
     */
    private void setSeckillGoodsTimeOut(String time, Long timeout) {

        // 每个时间段只要设置一次，设置商品过期时间计数器
        Long increment =
                redisTemplate.opsForHash().increment("Seckill_Goods_Expire_Count", time, 1);
        if (increment > 1) {
            return;
        }

        // 设置当前时间段的商品过期时间,，需要计算存活时间
        redisTemplate.expire(time, timeout, TimeUnit.MILLISECONDS);

        // 发送延迟消息,每个时间段只会发送一次延迟消息，商品活动结束后半小时进行数据同步的操作
        rabbitTemplate.convertAndSend(
                SeckillConst.SECKILL_GOODS_NORMAL_EXCHANGE,
                SeckillConst.SECKILL_GOODS_DEAD_ROUTING_KEY,
                time,
                message -> {
                    message.getMessageProperties().setExpiration((timeout + 30 * 60 * 1000) + "");
                    return message;
                });
    }
}
